using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;

namespace Sundstrom.Mvvm
{
    /// <summary>
    /// A type that implements INotifyPropertyChanged.
    /// </summary>
    public abstract class ObservableObject : INotifyPropertyChanged, ISupportingMethods
    {
        protected const string PropertyNotAMember = "The specified property is not a member of type \"{0}\".";
        protected internal const string PropertyExpressionExpected = "Expected property expression.";

        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Raises the PropertyChanged event for the specified property.
        /// </summary>
        /// <param name="propertyName">The name of the property.</param>
        protected virtual void RaisePropertyChanged([CallerMemberName] string propertyName = null)
        {
            Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(propertyName));

            if (((ISupportingMethods)this).HasProperty(propertyName))
            {
                PropertyChangedEventHandler handler = PropertyChanged;
                if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
            }
            else
            {
                throw new ArgumentException(string.Format(PropertyNotAMember, GetType().FullName), "propertyName");
            }
        }

        /// <summary>
        /// Raises the PropertyChanged event for the specified property.
        /// </summary>
        /// <param name="propertyExpression">An expression for a property.</param>
        /// <typeparam name="TProperty">The type of the property in the expression.</typeparam>
        protected void RaisePropertyChanged<TProperty>(Expression<Func<TProperty>> propertyExpression)
        {
            Contract.Requires<ArgumentException>(propertyExpression != null);

            var body = propertyExpression.Body;
            var memberExpression = body as MemberExpression;
            if (memberExpression != null && memberExpression.Member is PropertyInfo)
            {
                var member = memberExpression.Member;
                try
                {
                    RaisePropertyChanged(member.Name);
                }
                catch (ArgumentException e)
                {
                    throw new ArgumentException(e.Message, "propertyExpression");
                }
            }
            else
            {
                throw new ArgumentException(PropertyExpressionExpected, "propertyExpression");
            }
        }

        public void RaisePropertyChangedForAll()
        {
#if NET45
            IEnumerable<PropertyInfo> props = GetType().GetRuntimeProperties();
#else
            var props = GetType().GetProperties();
#endif

            foreach (PropertyInfo prop in props)
            {
#if NET45
                if (!prop.GetMethod.IsPublic)
                    continue;
#else
                if (!prop.GetGetMethod().IsPublic)
                    continue;
#endif
                RaisePropertyChanged(prop.Name);
            }
        }

        /// <summary>
        ///     Clears the value of the backing field of the specified property.
        /// </summary>
        /// <param name="propertyName"></param>
        protected void ClearPropertyValue([CallerMemberName] string propertyName = null)
        {
            Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(propertyName));

            if (((ISupportingMethods)this).HasProperty(propertyName))
            {
                var fieldInfo = GetBackingFieldInfo(propertyName);

#if NET45
                TypeInfo fieldType = fieldInfo.FieldType.GetTypeInfo();

#else
                Type fieldType = fieldInfo.FieldType;
#endif

                if (fieldType.IsValueType)
                {
                    fieldInfo.SetValue(this,
                        Activator.CreateInstance(fieldInfo.FieldType));
                }
                else
                {
                    fieldInfo.SetValue(this, null);
                }

                RaisePropertyChanged(propertyName);
            }
            else
            {
                throw new ArgumentException(string.Format(PropertyNotAMember, propertyName), "propertyName");
            }
        }

        /// <summary>
        ///     Clears the value of the backing field of the specified property.
        ///     
        /// </summary>
        /// <param name="propertyExpression"></param>
        protected void ClearPropertyValue<TProperty>(Expression<Func<TProperty>> propertyExpression)
        {
            Contract.Requires<ArgumentException>(propertyExpression != null);

            var body = propertyExpression.Body;
            var memberExpression = body as MemberExpression;
            if (memberExpression != null && memberExpression.Member is PropertyInfo)
            {
                var member = memberExpression.Member;
                try
                {
                   ClearPropertyValue(member.Name);
                }
                catch (ArgumentException e)
                {
                    throw new ArgumentException(e.Message, "propertyExpression");
                }
            }
            else
            {
                throw new ArgumentException(PropertyExpressionExpected, "propertyExpression");
            }
        }

        bool ISupportingMethods.HasProperty(string memberName)
        {
            MemberInfo memberInfo = null;
#if NET45
            memberInfo = GetType().GetRuntimeProperties().FirstOrDefault(x => x.Name == memberName);
            return memberInfo != null;
#else
            memberInfo = GetType().GetProperties().FirstOrDefault(x => x.Name == memberName);
            return memberInfo != null;
#endif

        }

        /// <summary>
        ///     Sets the value for the backing field of the specified property.
        /// </summary>
        /// <param name="propertyName"></param>
        /// <param name="value"></param>
        protected void SetPropertyValue<TProperty>(string propertyName, TProperty value)
        {
            Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(propertyName));

            if (((ISupportingMethods)this).HasProperty(propertyName))
            {
                var fieldInfo = GetBackingFieldInfo(propertyName);

                fieldInfo.SetValue(this, value);

                RaisePropertyChanged(propertyName);
            }
            else
            {
                throw new ArgumentException(string.Format(PropertyNotAMember, propertyName), "propertyName");
            }
        }

        /// <summary>
        /// Gets the backing field of the specified property.
        /// </summary>
        /// <param name="propertyName"></param>
        /// <returns></returns>
        protected FieldInfo GetBackingFieldInfo(string propertyName)
        {
            Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(propertyName));

            char firstLower = char.ToLower(propertyName[0]);
            string propertyFieldName = propertyName.Remove(0, 1);

            propertyFieldName = string.Format("_{0}",
                propertyFieldName.Insert(0, firstLower.ToString()));

#if NET45
            FieldInfo fieldInfo = GetType()
                .GetFields2()
                .First(x => x.Name == propertyFieldName);
#else
            FieldInfo fieldInfo = GetType()
                .GetFields2(BindingFlags.Instance | BindingFlags.NonPublic)
                .First(x => x.Name == propertyFieldName);
#endif

            return fieldInfo;
        }

        /// <summary>
        ///     Sets the value for the backing field of the specified property.
        /// </summary>
        /// <param name="propertyExpression"></param>
        /// <param name="value"></param>
        protected void SetPropertyValue<TProperty>(Expression<Func<TProperty>> propertyExpression, TProperty value)
        {
            Contract.Requires<ArgumentException>(propertyExpression != null);

            var body = propertyExpression.Body;
            var memberExpression = body as MemberExpression;
            if (memberExpression != null && memberExpression.Member is PropertyInfo)
            {
                var member = memberExpression.Member;
                try
                {
                    SetPropertyValue(member.Name, value);
                }
                catch (ArgumentException e)
                {
                    throw new ArgumentException(e.Message, "propertyExpression");
                }
            }
            else
            {
                throw new ArgumentException(PropertyExpressionExpected, "propertyExpression");
            }
        }
    }
}
